﻿using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;

namespace Narwhunderful.Aphelia.Framework.Net
{

    public class NarsockClient : BaseNarsock
    {

        public event EventHandler<NarsockDataArrivalEventArgs> DataArrival;
        public event EventHandler<NarsockSendEventArgs> SendProgress;
        public event EventHandler<NarsockSendEventArgs> SendComplete;

        Thread _sendThread;
        NarsockSendBuffer _sendBuffer = new NarsockSendBuffer();
        AutoResetEvent _resetSendEvent = new AutoResetEvent(false);

        byte[] _receiveBuffer = null;

        string _id = "";
        public string ID
        {
            get
            {
                return _id;
            }
            set
            {
                _id = value;
            }
        }

        string _remoteHost = "127.0.0.1";
        public string RemoteHost
        {
            get
            {
                return _remoteHost;
            }
            set
            {
                _remoteHost = value;
            }
        }

        IPAddress _resolvedIP = null;
        protected IPAddress ResolvedIP
        {
            get
            {
                return _resolvedIP;
            }
            set
            {
                _resolvedIP = value;
            }
        }

         public NarsockClient()
        {
            _sendThread = new Thread(new ThreadStart(this.SendThreadLoop));
            _sendThread.IsBackground = true;
            _sendThread.Start();
        }

        protected void SendThreadLoop()
        {
            while (_sendThread.IsAlive)
            {
                if (_sendBuffer.Count > 0)
                {
                    try
                    {
                        byte[] data = null;
                        lock (_sendBuffer.SyncRoot) 
                            data = _sendBuffer.Dequeue<byte[]>();
                        DoSend(data);
                    }
                    catch (SocketException ex)
                    {
                        Close();
                        OnExceptionReceived(new NarsockExceptionEventArgs(ex));
                    }
                }
                else
                {
                    _resetSendEvent.WaitOne();
                }
            }
        }

        public void Accept(Socket socket)
        {
            base.Socket = socket;
            IPEndPoint remoteEndPoint = (IPEndPoint)socket.RemoteEndPoint;
            _remoteHost = remoteEndPoint.Address.ToString();
            base.Port = remoteEndPoint.Port;
        }

        public void Connect()
        {
            try
            {
                _resolvedIP = null;
                if (IPAddress.TryParse(RemoteHost, out _resolvedIP))
                {
                    DoConnect();
                }
                else
                {
                    DoGetHost();
                }
            }
            catch (SocketException ex)
            {
                OnExceptionReceived(new NarsockExceptionEventArgs(ex));
            }
        }

        public void Send(byte[] data)
        {
            try
            {
                lock (_sendBuffer) _sendBuffer.Enqueue(data);
                _resetSendEvent.Set();
            }
            catch (SocketException ex)
            {
                OnExceptionReceived(new NarsockExceptionEventArgs(ex));
            }
        }

        protected virtual void DoConnect()
        {
            OnStateChanged(new NarsockStateChangedEventArgs(NarsockState.Connecting, base.State));
            try
            {
                base.Socket = new Socket(_resolvedIP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                base.Socket.BeginConnect(new IPEndPoint(this.ResolvedIP, Port), new AsyncCallback(ConnectCallback), base.Socket);
            }
            catch (SocketException ex)
            {
                OnExceptionReceived(new NarsockExceptionEventArgs(ex));
            }
        }

        void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                if (base.State == NarsockState.Connecting)
                {
                    Socket socket = (Socket)ar.AsyncState;
                    socket.EndConnect(ar);
                    DoReceive();
                    OnStateChanged(new NarsockStateChangedEventArgs(NarsockState.Connected, base.State));
                }
                else
                {
                    Close();
                }
            }
            catch (SocketException ex)
            {
                if (ex.SocketErrorCode != SocketError.Success)
                {
                    Close();
                    OnExceptionReceived(new NarsockExceptionEventArgs(ex));
                }
            }
        }

        protected virtual void DoGetHost()
        {
            OnStateChanged(new NarsockStateChangedEventArgs(NarsockState.ResolvingHost, base.State));
            Dns.BeginGetHostEntry(RemoteHost, new AsyncCallback(HostCallback), Port);
        }

        void HostCallback(IAsyncResult ar)
        {
            IPHostEntry resolved = null;
            int port = (int)ar.AsyncState;
            try
            {
                resolved = Dns.EndGetHostEntry(ar);
            }
            catch (SocketException ex)
            {
                if (ex != null) resolved = null;
            }
            finally
            {
                if (resolved == null) throw new Exception("Hostname could not be resolved.");
                if (resolved.AddressList.Length <= 0) throw new Exception("Hostname '" + resolved.HostName + "' could not be resolved.");
                _resolvedIP = resolved.AddressList[0];
                OnStateChanged(new NarsockStateChangedEventArgs(NarsockState.HostResolved, base.State));
            }
        }

        protected virtual void DoReceive()
        {
            try
            {
                SocketError e = SocketError.Success;
                base.Socket.BeginReceive(_receiveBuffer, 0, base.BufferSize, SocketFlags.None, out e, new AsyncCallback(ReceiveCallback), e);
            }
            catch (SocketException ex)
            {
                OnExceptionReceived(new NarsockExceptionEventArgs(ex));
            }
        }

        void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                int size = base.Socket.EndReceive(ar);
                if (size >= 1)
                {
                    IPEndPoint ep = (IPEndPoint)base.Socket.RemoteEndPoint;
                    ResizeByteArray(ref _receiveBuffer, size);
                    NarsockDataArrivalEventArgs e = new NarsockDataArrivalEventArgs((byte[])_receiveBuffer.Clone());
                    OnDataArrival(e);
                    _receiveBuffer = new byte[base.BufferSize + 1];
                    DoReceive();
                }
            }
            catch (SocketException ex)
            {
                if (ex.SocketErrorCode != SocketError.Success)
                {
                    Close();
                    OnExceptionReceived(new NarsockExceptionEventArgs(ex));
                }
            }
            catch (ObjectDisposedException ex2)
            {
                if (ex2 != null) return;
            }
        }

        protected virtual void DoSend(byte[] data)
        {
            int startIndex = 0;
            while (startIndex < data.Length)
            {
                int left = data.Length - startIndex;
                if (left > this.BufferSize)
                {
                    base.Socket.Send(data, startIndex, base.BufferSize, SocketFlags.None);
                    startIndex += this.BufferSize;
                    OnSendProgress(new NarsockSendEventArgs(data.Length, startIndex));
                }
                else
                {
                    base.Socket.Send(data, startIndex, left, SocketFlags.None);
                    startIndex += left;
                    OnSendComplete(new NarsockSendEventArgs(data.Length, startIndex));
                }
            }
        }

        void ResizeByteArray(ref byte[] byteArray, int size)
        {
            if ((size - 1) < byteArray.GetUpperBound(0))
            {
                byte[] data = null;
                Array.Copy(byteArray, 0, data, 0, size);
                byteArray = data;
            }
        }

        protected virtual void OnDataArrival(NarsockDataArrivalEventArgs e)
        {
            DataArrival.SafeCrossThreadInvoke(this, e);
        }

        protected virtual void OnSendComplete(NarsockSendEventArgs e)
        {
            SendComplete.SafeCrossThreadInvoke(this, e);
        }

        protected virtual void OnSendProgress(NarsockSendEventArgs e)
        {
            SendProgress.SafeCrossThreadInvoke(this, e);
        }

    }

}
